/*
 * PropertyChangedAspect.java
 *
 * Created on 08. Oktober 2007, 22:33
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package net.entelijan.cobean.bind.impl;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import net.entelijan.cobean.bind.IModelChangeListener;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;

/**
 * 
 * @author wwagner4
 */
class PropertyChangedAdvice implements MethodInterceptor, IModelChangeListenersAware {

    private static Log log = LogFactory.getLog(PropertyChangedAdvice.class);

    private List<IModelChangeListener> modelChangeListeners;

    /** Creates a new instance of PropertyChangedAspect */
    public PropertyChangedAdvice() {
        super();
    }

    public Object invoke(final MethodInvocation invoc) throws Throwable {
        Object re;
        String methodName = invoc.getMethod().getName();
        log.debug("[invoke] method:'" + methodName + "' of '" + invoc.getThis() + "' arg:'"
                + Arrays.toString(invoc.getArguments()) + "'");
        if (methodName.startsWith("set") || methodName.startsWith("add")
                || methodName.startsWith("remove")) {
            Object val = getValue(invoc);
            Object arg = getArgument(0, invoc);
            for (IModelChangeListener modelChangeListener : this.modelChangeListeners) {
                modelChangeListener.prepareComponent(val, arg);
            }
            re = invoc.proceed();
            // Update the component if the old value could not be retrieved or if it is
            // not equal to the new value.
            if (!equals(val, arg)) {
                PropertyDescriptor propDesc = BeanUtils.findPropertyForMethod(invoc.getMethod());
                if (log.isDebugEnabled()) {
                    log.debug("[invoke] update " + this.modelChangeListeners.size()
                            + " listeners because value was changed from '" + val + "' to '" + arg
                            + "'");
                }
                int count = 1;
                for (IModelChangeListener modelChangeListener : this.modelChangeListeners) {
                    if (propDesc != null) {
                        // Only update the components of the property that was changed
                        if (modelChangeListener.getPropertyName().equals(propDesc.getName())) {
                            log.debug("[invoke] update " + count++
                                    + " component of modelChangeListener='" + modelChangeListener
                                    + "'");
                            modelChangeListener.updateComponent();
                        }
                    } else { // This is not a property but an method like 'add' or 'remove'
                        log
                                .debug("[invoke] update " + count++
                                        + " component of modelChangeListener='"
                                        + modelChangeListener + "'");
                        modelChangeListener.updateComponent();
                    }
                }
            } else if (log.isDebugEnabled()) {
                log.debug("[invoke] not updating " + this.modelChangeListeners.size()
                        + " components of " + invoc.getThis() + " because old value (" + val
                        + ") equals new value (" + arg + ")");
            }
        } else {
            throw new IllegalStateException("[invoke] The advice should not be called for '"
                    + methodName + "'. The advisor should permit only setters, add and remove");
        }
        return re;
    }

    private boolean equals(Object o1, Object o2) {
        boolean re = false;
        if (o1 == null && o2 == null) {
            re = true;
        } else if (o1 != null && o2 != null) {
            re = o1.equals(o2);
        }
        return re;
    }

    private Object getArgument(int index, MethodInvocation invoc) {
        Object re = null;
        Object[] arguments = invoc.getArguments();
        if (arguments != null && arguments.length > index) {
            re = arguments[index];
        }
        return re;
    }

    /**
     * Gets the value if the method was a setter. In every other case null is
     * returned. That means new and old value cannot be compared. In that case
     * the component gets also updated if the value was not changed.
     * 
     */
    private Object getValue(MethodInvocation invoc) {
        Object re = null;
        String setterName = invoc.getMethod().getName();
        String getterName = getterNameFromSetterName(setterName);
        log.debug("[getValue] setterName='" + setterName + "' getterName='" + getterName + "'");
        try {
            Method getter = findMethod(invoc.getThis().getClass(), getterName);
            if (getter != null) {
                re = getter.invoke(invoc.getThis(), new Object[0]);
            }
        } catch (IllegalArgumentException e) {
            // nothing to be done. return null
        } catch (IllegalAccessException e) {
            // nothing to be done. return null
        } catch (InvocationTargetException e) {
            // nothing to be done. return null
        }
        log.debug("[getValue] re:'" + re + "'");
        return re;
    }

    private String getterNameFromSetterName(String string) {
        return "get" + string.substring(3);
    }

    private Method findMethod(Class<?> clazz, String name) {
        Method ret = null;
        for (int i = 0; i < clazz.getMethods().length; i++) {
            Method m = clazz.getMethods()[i];
            if (m.getName().equals(name) && m.getParameterTypes().length == 0) {
                ret = m;
                break;
            }
        }
        return ret;
    }

    public List<IModelChangeListener> getModelChangeListeners() {
        return this.modelChangeListeners;
    }

    public void setModelChangeListeners(List<IModelChangeListener> modelChangeListeners) {
        this.modelChangeListeners = modelChangeListeners;
    }

}
